Imports System.Collections
Imports System.Collections.Generic
Imports System.IO
Imports System.Reflection
Namespace Core
    Friend Class DbfFile
        Inherits ClusteredFile
        Implements IHasEncoding
        Public ReadOnly Property RecordCount() As Int32
            Get
                Return mRecordCount
            End Get
        End Property
        Protected mRecordWriter As QuickSerializer
        Protected Friend mEncoding As System.Text.Encoding
        Friend mVersion As DbfVersion
        Protected Shared mDbfHeaderWriter As QuickSerializer
        Protected Shared mDbfColumnWriter As QuickSerializer
        Friend mMemoFile As DbtFile
        Protected mDbfHeader As DbfHeader
        Protected END_OF_COLUMN_DEFINITIONS As Byte = 13

        Shared Sub New()
            mDbfHeaderWriter = New QuickSerializer(GetType(DbfHeader), DbfVersion.DbfDotNet, Nothing, 0, False, True)
            mDbfColumnWriter = New QuickSerializer(GetType(DbfColumnHeader), DbfVersion.DbfDotNet, Nothing, 0, False, True)
        End Sub

        Friend ReadOnly Property Columns() As List(Of ColumnDefinition)
            Get
                Return mColumns
            End Get
        End Property

        Friend Overridable Function GetIndex(filepath As String, sortOrder As SortOrder) As DbfIndex
            Return Nothing
        End Function

        Friend Function GetColumn(columnName As String) As ColumnDefinition
            Dim columns As List(Of ColumnDefinition) = Me.mColumns
            For i As Integer = 0 To columns.Count - 1
                Dim column As ColumnDefinition = columns(i)
                If String.Equals(column.ColumnName, columnName, StringComparison.OrdinalIgnoreCase) Then
                    Return column
                End If
            Next
            Return Nothing
        End Function

        Friend Overloads Shared Function [Get](filePath As String, encoding As System.Text.Encoding, version As DbfVersion) As DbfFile
            Dim file As Core.DbfFile = ClusteredFile.[Get](Of DbfFile)(filePath, OpenFileMode.OpenOrCreate, 4096)
            If file.ReaderCount = 1 Then
                file.mEncoding = encoding
                file.mVersion = version
                file.Initialize()
                ' perhaps check encoding and version match 
            Else
            End If
            Return file
        End Function


        Protected Overrides Function OnCreateNewRecord(isNew As Boolean, recordNo As Int32) As Record
            Return New DbfRecord()
        End Function

        Protected Overrides Sub OnReadRecordBuffer(buffer As Byte(), record As Record)
            mRecordWriter.Read(Me, buffer, record)
        End Sub

        Friend Overrides Function OnFillWriteBuffer(record As Record, buffer As Byte()) As Boolean
            mRecordWriter.Write(Me, buffer, record)
            Return True
        End Function

        Protected Overrides Sub OnInitialize(stream As Stream)

            Dim headerBuff As [Byte]() = New Byte(mDbfHeaderWriter.RecordWidth - 1) {}
            mDbfHeader = New DbfHeader()
            Dim read As Integer = stream.Read(headerBuff, 0, headerBuff.Length)

            If read > 0 Then
                mDbfHeaderWriter.Read(Me, headerBuff, mDbfHeader)
                'File size reported by the operating system must match the logical file size. Logical file size = ( Length of header + ( Number of records * Length of each record ) ) 
                'mHeaderSerializer.mQuickWriteMethod(this, headerBuff, header);
                Dim nbColumns As Int32 = ((mDbfHeader.HeaderWidth - mDbfHeaderWriter.RecordWidth) \ mDbfColumnWriter.RecordWidth) + 1
                Dim totalLength As Int64 = stream.Length
                mRecordWidth = mDbfHeader.RecordWidth
                Dim recordsLength As Int64 = totalLength - mDbfHeader.HeaderWidth
                If mRecordWidth > 0 Then
                    Dim columns As List(Of ColumnDefinition) = New List(Of ColumnDefinition)(nbColumns)
                    mRecordCount = CType(recordsLength \ mRecordWidth, Int32)

                    Dim columnHeaderBuff As [Byte]() = New Byte(mDbfColumnWriter.RecordWidth - 1) {}
                    Dim dbfColumnHeader As New DbfColumnHeader()
                    Dim columnDefinition As ColumnDefinition = Nothing
                    'new ColumnDefinition();
                    'columnDefinition.Initialize(DbfVersion.dBaseIII, 0, typeof(DbfRecord).GetField("DeletedFlag",
                    '    BindingFlags.Instance
                    '   | BindingFlags.Public
                    '   | BindingFlags.NonPublic
                    '   | BindingFlags.DeclaredOnly), 0, null);
                    'columns.Add(columnDefinition);

                    For i As Integer = 1 To nbColumns - 1
                        read = stream.Read(columnHeaderBuff, 0, columnHeaderBuff.Length)
                        mDbfColumnWriter.Read(Me, columnHeaderBuff, dbfColumnHeader)
                        '
                        '                        System.Diagnostics.Trace.WriteLine(string.Format("{0},{1},{2},{3}",
                        '                            dbfColumnHeader.ColumnName,
                        '                            dbfColumnHeader.ColumnType,
                        '                            dbfColumnHeader.ColumnWidth,
                        '                            dbfColumnHeader.Decimals));
                        '                        /*

                        columnDefinition = New ColumnDefinition(dbfColumnHeader.ColumnName, dbfColumnHeader.ColumnType, dbfColumnHeader.ColumnWidth, dbfColumnHeader.Decimals)
                        columns.Add(columnDefinition)
                    Next
                    Dim lastByte As Integer = stream.ReadByte()
                    If lastByte = END_OF_COLUMN_DEFINITIONS Then
                        mColumns = columns
                        mHeaderWidth = mDbfHeader.HeaderWidth
                    End If
                End If
            End If
            'ignoremissingfields
            'setoffset
            mRecordWriter = New QuickSerializer(RecordType(), mVersion, mColumns, mRecordWidth, False, True)

            If mColumns Is Nothing Then
                mColumns = mRecordWriter.Columns
                mRecordWidth = mRecordWriter.RecordWidth

                mDbfHeader.VerNumber = 131
                mDbfHeader.LastUpdate = DateTime.Now
                mDbfHeader.NbRecords = 0
                Dim nbColumns As Integer = mColumns.Count
                mDbfHeader.HeaderWidth = CType(mDbfHeaderWriter.RecordWidth + mDbfColumnWriter.RecordWidth * nbColumns + 1, UInt16)
                mDbfHeader.RecordWidth = CType(mRecordWriter.RecordWidth, UInt16)
                mDbfHeader.Zero = 0
                mDbfHeader.IncompleteTransaction = 0
                mDbfHeader.EncryptionFlag = 0
                'mDbfHeader.LanOnly = [];
                mDbfHeader.Indexed = 0
                mDbfHeader.Language = 0
                mDbfHeader.Zero2 = 0

                'withColumns
                WriteHeader(stream, True)
            End If

            Dim hasMemo As Boolean = False
            For i As Integer = 0 To mColumns.Count - 1
                Dim c As ColumnDefinition = mColumns(i)
                If c.IsMemo() Then
                    hasMemo = True
                    Exit For
                End If
            Next
            If hasMemo Then
                mMemoFile = ClusteredFile.[Get](Of DbtFile)(System.IO.Path.ChangeExtension(mOriginalFile, ".dbt"), mOpenFileMode, 512)
                mMemoFile.mDbfFile = Me

                mMemoFile.Initialize()
            End If
        End Sub

        Private Overloads Sub WriteHeader(stream As Stream, withColumns As Boolean)
            mDbfHeader.NbRecords = CType(mRecordCount, Int32)

            Dim newHeaderWith As UInt16 = CType(mDbfHeaderWriter.RecordWidth + (mColumns.Count - 1) * mDbfColumnWriter.RecordWidth + 1, UInt16)
            mDbfHeader.HeaderWidth = newHeaderWith
            mHeaderWidth = newHeaderWith

            Dim headerBuff As [Byte]() = New Byte(mDbfHeaderWriter.RecordWidth - 1) {}
            mDbfHeaderWriter.Write(Me, headerBuff, mDbfHeader)

            stream.Seek(0, SeekOrigin.Begin)
            stream.Write(headerBuff, 0, headerBuff.Length)
#If DUMP_DISK_ACTIVITY Then
			System.Diagnostics.Debug.WriteLine(mOriginalFile & " WriteHeader")
			System.Diagnostics.Debug.WriteLine(Utils.HexDump(headerBuff))
#End If

            If withColumns Then
                Dim columnHeaderBuff As [Byte]() = New Byte(mDbfColumnWriter.RecordWidth - 1) {}
                Dim dbfColumnHeader As New DbfColumnHeader()
                ' we ignore column 0
                For i As Integer = 1 To mColumns.Count - 1
                    Dim c As ColumnDefinition = mColumns(i)
                    dbfColumnHeader.ColumnName = c.ColumnName
                    dbfColumnHeader.ColumnType = c.DbfColumnType
                    dbfColumnHeader.ColumnWidth = CByte(c.ExtendedProperties("ColumnWidth"))
                    dbfColumnHeader.Decimals = CByte(c.ExtendedProperties("Decimals"))

                    mDbfColumnWriter.Write(Me, columnHeaderBuff, dbfColumnHeader)
                    stream.Write(columnHeaderBuff, 0, columnHeaderBuff.Length)
#If DUMP_DISK_ACTIVITY Then
					System.Diagnostics.Debug.WriteLine(mOriginalFile & " WriteColumn #" & i)
#End If

                    System.Diagnostics.Debug.WriteLine(Utils.HexDump(columnHeaderBuff))
                Next
                stream.WriteByte(END_OF_COLUMN_DEFINITIONS)
            End If
            MyBase.mCurrentRecordNoPosition = Int32.MaxValue
        End Sub

        Protected Overrides Sub OnWriteHeader(stream As Stream)
            WriteHeader(stream, False)
        End Sub

        Protected Overridable Function RecordType() As Type
            Return GetType(DbfRecord)
        End Function

#Region "IHasEncoding Members"

        Public ReadOnly Property Encoding() As System.Text.Encoding Implements IHasEncoding.Encoding
            Get
                Return mEncoding
            End Get
        End Property

#End Region

    End Class

    Friend NotInheritable Class DbfFile(Of TRecord As {DbfRecord, New})
        Inherits DbfFile
        Implements IEnumerable(Of TRecord)
        Friend mIndexes As List(Of NdxFile(Of TRecord))

        Public Sub New()
            mIndexes = New List(Of NdxFile(Of TRecord))()
        End Sub

        Public ReadOnly Property Version() As DbfVersion
            Get
                Return mVersion
            End Get
        End Property

        Public Shadows Function NewRecord() As TRecord
            Return DirectCast(MyBase.NewRecord(), TRecord)
        End Function


        Friend Overrides Sub OnWriteBufferModified(recordNo As Int32, oldBuffer As Byte(), newBuffer As Byte())
            System.Diagnostics.Debug.Assert(recordNo < Int32.MaxValue, "Invalid Record Number.")
            For i As Integer = 0 To mIndexes.Count - 1
                Dim index As NdxFile(Of TRecord) = mIndexes(i)
                index.OnDbfRecordModified(recordNo, oldBuffer, newBuffer)
            Next
        End Sub

        Protected Overrides Function OnCreateNewRecord(isNew As Boolean, recordNo As Int32) As Record
            Return New TRecord()
        End Function


#Region "IEnumerable<TRecord> Members"

        Public Iterator Function GetEnumerator() As IEnumerator(Of TRecord)
            ' IEnumerable<TRecord>.
            For i As Int32 = 0 To mRecordCount - 1
                Dim r As TRecord = InternalGetRecord(i)
                Yield r
            Next
        End Function
        Private Function Get_Enumerator() As IEnumerator Implements IEnumerable.GetEnumerator
            Return GetEnumerator()
        End Function
        Public Overloads Function InternalGetRecord(recordNo As Int32) As TRecord
            Dim r As TRecord = DirectCast(InternalGetRecord(recordNo, False), TRecord)
            Return r
        End Function
#End Region

#Region "IEnumerable<TRecord> Members"

        Private Function IEnumerable_GetEnumerator() As IEnumerator(Of TRecord) Implements IEnumerable(Of TRecord).GetEnumerator
            Throw New NotImplementedException()
        End Function

#End Region

        Protected Overrides Sub OnDisposing()
            MyBase.OnDisposing()
            For i As Integer = 0 To mIndexes.Count - 1
                mIndexes(i).Flush()
            Next
            mIndexes.Clear()
        End Sub

        Friend Shared Shadows Function [Get](filePath As String, encoding As System.Text.Encoding, version As DbfVersion) As DbfFile(Of TRecord)
            Dim file As DbfFile(Of TRecord) = ClusteredFile.[Get](Of DbfFile(Of TRecord))(filePath, OpenFileMode.OpenOrCreate, 4096)
            If file.ReaderCount = 1 Then
                file.mEncoding = encoding
                file.mVersion = version
                file.Initialize()
                ' perhaps check encoding and version match 
            Else
            End If
            Return file
        End Function

        Friend Overrides Function GetIndex(filepath As String, sortOrder As SortOrder) As DbfIndex
            Dim indexTable As NdxFile(Of TRecord) = NdxFile(Of TRecord).[Get](filepath, Me, sortOrder)
            If indexTable.mDbfIndex Is Nothing Then
                indexTable.mDbfIndex = New DbfIndex(Of TRecord)()
                indexTable.mDbfFile = Me
                indexTable.mDbfIndex.mIndexTable = indexTable
            End If
            Return DirectCast(indexTable.mDbfIndex, DbfIndex)
        End Function

        Protected Overrides Function RecordType() As Type
            Return GetType(TRecord)
        End Function

    End Class
End Namespace
